/*
 *    Geotoolkit - An Open Source Java GIS Toolkit
 *    http://www.geotoolkit.org
 *
 *    (C) 2013, Geomatys
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotoolkit.style.io;

import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.geotoolkit.filter.FilterUtilities;
import org.geotoolkit.io.SimpleFileFilter;
import org.geotoolkit.style.DefaultStyleFactory;
import org.geotoolkit.style.MutableStyleFactory;
import org.geotoolkit.style.StyleConstants;
import static org.geotoolkit.style.StyleConstants.DEFAULT_CATEGORIZE_LOOKUP;
import static org.geotoolkit.style.StyleConstants.DEFAULT_FALLBACK;
import org.geotoolkit.style.function.InterpolationPoint;
import org.geotoolkit.style.function.Method;
import org.geotoolkit.style.function.Mode;
import org.geotoolkit.style.function.ThreshholdsBelongTo;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.Expression;
import org.opengis.style.ColorMap;

/**
 * Palette reader based on pattern.
 * template :
 * - regex ignore line
 * (newline)
 * - values pattern (v1,r1,g1,b1,v2,r2,g2,b2)
 *
 * CPT are generated by tools : grd2cpt
 * CLR are generated by tools : Argis ColorRamp2.0
 * PAL are generated by various tools with various patterns, this is just one of the possibilities.
 *
 * Ignore lines are tested if they contain a text like :
 * COLOR_MODEL = HSV
 * COLOR_MODEL = +HSV
 * If so HSV colors are used.
 *
 * @author Johann Sorel (Geomatys)
 */
public class PaletteReader {

    public static final SimpleFileFilter FILE_FILTER = new SimpleFileFilter("Palette",false,new String[]{"clr","cpt","pal"});

    protected static final FilterFactory FF = FilterUtilities.FF;
    protected static final MutableStyleFactory SF = DefaultStyleFactory.provider();

    private static class Row implements Comparable<Row> {
        Double v1 = null;
        Double v2 = null;
        Double r1 = null;
        Double r2 = null;
        Double g1 = null;
        Double g2 = null;
        Double b1 = null;
        Double b2 = null;

        @Override
        public int compareTo(Row other) {
            if (v1 == null) return -1;
            return v1.compareTo(other.v1);
        }

        public Color getColor1(boolean hsv) {
            if (hsv) {
                return Color.getHSBColor(r1.floatValue() / 360.0f, g1.floatValue(), b1.floatValue());
            } else {
                return new Color(r1.intValue(), g1.intValue(), b1.intValue());
            }
        }

        public Color getColor2(boolean hsv) {
            if (hsv) {
                return Color.getHSBColor(r2.floatValue() / 360.0f, g2.floatValue(), b2.floatValue());
            } else {
                return new Color(r2.intValue(), g2.intValue(), b2.intValue());
            }
        }

    }

    public static final String PATTERN_CLR = "^ColorMap.* \n v1 r1 g1 b1";
    public static final String PATTERN_CPT = "^(#|B|F|N).* \n v1 r1 g1 b1 v2 r2 g2 b2";
    public static final String PATTERN_PAL = "r1,g1,b1,\"v2 ?- ?v1\"";

    private final Pattern[] ignorePatterns;
    private final String valuePattern;
    private final boolean categorize;
    private final Pattern valStart;

    /**
     *
     * @param pattern
     */
    public PaletteReader(String pattern) {
        final String[] parts = pattern.split("\n");
        ignorePatterns = new Pattern[parts.length-1];
        for (int i = 0, n = parts.length-1; i < n; i++) {
            ignorePatterns[i] = Pattern.compile(parts[i].trim());
        }
        valuePattern = parts[parts.length-1].trim();
        categorize = valuePattern.contains("v2");
        valStart = Pattern.compile("^(v1|v2|r1|r2|g1|g2|b1|b2).*");
    }

    public ColorMap read(String candidate) throws IOException {
        final String[] parts = candidate.split("\n");

        boolean isHSV = false;

        final List<Row> rows = new ArrayList<>();
        lines:
        for (String part : parts) {
            part = part.trim();
            if (part.isEmpty()) continue lines;

            //check if we ignore this line
            for (Pattern p  : ignorePatterns) {
                if (p.matcher(part).matches()) {
                    //check for something like :
                    // COLOR_MODEL = +HSV
                    // COLOR_MODEL = HSV
                    isHSV |= part.contains("COLOR_MODEL") && part.contains("HSV");
                    continue lines;
                }
            }

            //parse values
            String pattern = valuePattern;
            final Row row = new Row();

            while (!part.isEmpty()) {
                boolean optional = false;
                if (pattern.isEmpty()) {
                    //ignore anything remaining
                    break;
                } else if (pattern.charAt(0) == '?') {
                    optional = true;
                    pattern = pattern.substring(1);
                }

                if (!valStart.matcher(pattern).matches()) {
                    char c = pattern.charAt(0);
                    if (c == ' ') {
                        part = part.trim();
                    } else {
                        char v = part.charAt(0);
                        if (v == c) {
                            part = part.substring(1);
                        } else if (!optional) {
                            throw new IOException("Pattern do not match.");
                        }

                    }
                    pattern = pattern.substring(1);
                    continue;
                }

                //we work with a value
                final int numberEnd = numberEnd(part);
                if (numberEnd == 0) {
                    pattern = pattern.substring(2);
                    if (optional) {
                        continue;
                    } else {
                        throw new IOException("Pattern do not match.");
                    }
                }

                Double val = parseDouble(part, numberEnd);
                part = part.substring(numberEnd);

                if (pattern.startsWith("v1")) row.v1 = val;
                if (pattern.startsWith("v2")) row.v2 = val;
                if (pattern.startsWith("r1")) row.r1 = val;
                if (pattern.startsWith("r2")) row.r2 = val;
                if (pattern.startsWith("g1")) row.g1 = val;
                if (pattern.startsWith("g2")) row.g2 = val;
                if (pattern.startsWith("b1")) row.b1 = val;
                if (pattern.startsWith("b2")) row.b2 = val;

                pattern = pattern.substring(2);
            }
            rows.add(row);
        }

        //sort values in ascending order
        Collections.sort(rows);

        final ColorMap colorMap;
        if (!categorize) {
            //interpolated color model
            final List<InterpolationPoint> values = new ArrayList<>();
            values.add( SF.interpolationPoint(Double.NaN, SF.literal(new Color(0,0,0,0))));
            for (Row row : rows) {
                values.add( SF.interpolationPoint(row.v1, SF.literal(row.getColor1(isHSV))));
            }
            final Expression function = SF.interpolateFunction(DEFAULT_CATEGORIZE_LOOKUP,
                    values, Method.COLOR, Mode.LINEAR, DEFAULT_FALLBACK);
            colorMap = SF.colorMap(function);

        } else {
            //categorize color model
            final Map<Expression, Expression> values = new HashMap<>();

            for (int i = 0, n = rows.size(); i < n; i++) {
                final Row row = rows.get(i);

                if (values.isEmpty()) {
                    if (row.v1 == null) {
                        values.put( StyleConstants.CATEGORIZE_LESS_INFINITY, SF.literal(row.getColor1(isHSV)));
                    } else {
                        //add a translucent range from -infinity to value
                        values.put( StyleConstants.CATEGORIZE_LESS_INFINITY, SF.literal(new Color(0f,0f,0f,0f)));
                        values.put( FF.literal(row.v1), SF.literal(row.getColor1(isHSV)));
                    }
                } else {
                    //two values, two colors
                    final Row lastRow = rows.get(i - 1);
                    if (lastRow.v2.doubleValue() == row.v1.doubleValue() && lastRow.r2 != null && !lastRow.getColor2(isHSV).equals(row.getColor1(isHSV))) {
                        //a color change at exact value, add a row for this one
                        values.put( FF.literal(lastRow.v2), SF.literal(lastRow.getColor2(isHSV)));
                        values.put( FF.literal(Math.nextUp(row.v1)), SF.literal(row.getColor1(isHSV)));
                    } else {
                        values.put( FF.literal(row.v1), SF.literal(row.getColor1(isHSV)));
                    }
                }

                //special case for last element
                if (i == n-1) {
                    if (row.r2 == null) {
                        values.put( FF.literal(row.v2), SF.literal(new Color(0f,0f,0f,0f)));
                    } else {
                        values.put( FF.literal(row.v2), SF.literal(row.getColor2(isHSV)));
                    }

                    //add a NaN group
                    values.put( FF.literal(Double.NaN), SF.literal(new Color(0f,0f,0f,0f)));
                }
            }
            final Expression function = SF.categorizeFunction(DEFAULT_CATEGORIZE_LOOKUP,
                    values, ThreshholdsBelongTo.SUCCEEDING, DEFAULT_FALLBACK);
            colorMap = SF.colorMap(function);
        }

        return colorMap;
    }

    private static double parseDouble(String candidate, int end) {
        String str = candidate.substring(0,end);
        return Double.parseDouble(str);
    }

    private static int numberEnd(String candidate) throws IOException {
        int end=0;
        //possible negation
        if (candidate.charAt(0) == '-') {
            end++;
        }

        while (candidate.length() > end && (Character.isDigit(candidate.charAt(end)) || candidate.charAt(end) == '.')) {
            end++;
        }
        return end;
    }

}
